/*
 * File     : AutoFree.java
 * All Rights Reserved.
 */
package com.googlecode.dni.type;

import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;

import com.googlecode.dni.Allocator;
import com.googlecode.dni.internal.DniInternal;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * <p>
 *  Utility class for automatically freeing resources.
 * </p>
 * <p>
 *  {@link FreeableNativeObject}s and other {@link Freeable}s can be added to
 *  an instance.  When the instance is {@linkplain #free() freed}, all the
 *  objects added will be freed.  Furthermore, when the instance is finalized
 *  after failing to be freed, it free all the objects.
 * </p>
 * <p>
 *  Adding the same {@link Freeable} is idempotent, where 'the same' means that
 *  the object {@linkplain Object#equals(Object) equals} the other object.
 *  Adding the same {@link NativeObject} is also idempotent, where 'the same'
 *  means that the object
 *  {@linkplain NativeObject#isSameObject(NativeObject) has the same address} as
 *  the other object.
 *  Adding the same {@link Pointer} is also idempotent.  However, the pointer is
 *  not considered comparable with a {@link NativeObject}; hence, adding a
 *  {@link NativeObject} and its pointer will cause the object to be freed
 *  twice.
 * </p>
 * <p>
 *  Adding a {@link FreeableNativeObject} through either method has the same
 *  effect.
 * </p>
 * <p>
 *  Other Java objects can be added to keep them alive until this instance is
 *  freed.  For example, it is necessary to keep a reference to a
 *  {@link ByteBuffer} instance as long as the native memory is required.
 * </p>
 * <p>
 *  There is no guarantee concerning the order in which objects will be freed.
 * </p>
 * <p>
 *  Instances of this class are not safe to be used from multiple threads
 *  without external synchronization.
 * <p>
 *  In Java 7, this can be used with the enhanced 'try with resources' block.
 * </p>
 *
 * @author Matthew Wilson
 */
// TODO v1.1 Java 7 support (AutoCloseable)
public final class AutoFree implements Freeable //, AutoCloseable
{

    private final Map< Freeable, Object > freeables = new HashMap< Freeable, Object >();

    @SuppressFBWarnings( value = "WOC_WRITE_ONLY_COLLECTION_FIELD",
                         justification = "It's designed to retain strong references only" )
    private final List< Object > javaObjects = new ArrayList< Object >();


    /**
     * <p>
     *  Adds a native object to be freed when this freeable is freed.
     * </p>
     *
     * @param <T>
     *            the object type
     * @param nativeObject
     *            the native object
     *
     * @return the same object
     */
    public < T extends FreeableNativeObject > T add( final T nativeObject )
    {
        if ( nativeObject == null )
        {
            throw new NullPointerException();
        }

        this.freeables.put( new NativeObjectWrapper( nativeObject ), null );
        return nativeObject;
    }

    /**
     * <p>
     *  Adds a freeable to be freed when this freeable is freed.
     * </p>
     *
     * @param <T>
     *            the freeable type
     * @param freeable
     *            the freeable
     *
     * @return the same freeable
     */
    public < T extends Freeable > T add( final T freeable )
    {
        if ( freeable == this )
        {
            throw new IllegalArgumentException();
        }

        if ( freeable instanceof FreeableNativeObject )
        {
            add( (FreeableNativeObject) freeable );
        }
        else
        {
            if ( freeable == null )
            {
                throw new NullPointerException();
            }

            this.freeables.put( freeable, null );
        }
        return freeable;
    }

    /**
     * <p>
     *  Adds a pointer to native memory that will be freed when this freeable is
     *  freed.
     * </p>
     * <p>
     *  The memory is freed by calling {@link Allocator#free(Pointer)} with the
     *  default {@link Allocator}.
     * </p>
     *
     * @param pointer
     *            the pointer
     *
     * @return the same pointer
     */
    public Pointer add( final Pointer pointer )
    {
        this.freeables.put( new PointerWrapper( pointer ), null );
        return pointer;
    }

    /**
     * <p>
     *  Adds a Java object to keep alive until at least this freeable is
     *  freed.
     * </p>
     * <p>
     *  This method cannot accept a {@link Freeable} object.
     * </p>
     *
     * @param object
     *            the Java object
     */
    public void keepAlive( final Object object )
    {
        if ( object instanceof Freeable )
        {
            throw new IllegalArgumentException( "Don't use keepAlive for Freeable types" );
        }

        this.javaObjects.add( object );
    }

    /** {@inheritDoc} */
    @Override
    public void free()
    {
        this.javaObjects.clear();

        for ( Freeable freeable : this.freeables.keySet() )
        {
            try
            {
                freeable.free();
            }
            catch ( Throwable throwable )
            {
                DniInternal.LOGGER.log( Level.SEVERE, "Failed to free: " + freeable, throwable );
            }
        }
        this.freeables.clear();
    }

    @Override
    protected void finalize()
    {
        free();
    }

    private static final class NativeObjectWrapper implements Freeable
    {
        private final FreeableNativeObject nativeObject;

        NativeObjectWrapper( final FreeableNativeObject nativeObject )
        {
            this.nativeObject = nativeObject;
        }

        @Override
        public void free()
        {
            this.nativeObject.free();
        }

        /** {@inheritDoc} */
        @Override
        public boolean equals( final Object obj )
        {
            if ( obj == null || obj.getClass() != getClass() )
            {
                return false;
            }
            if ( obj == this )
            {
                return true;
            }
            NativeObjectWrapper other = (NativeObjectWrapper) obj;
            return this.nativeObject.isSameObject( other.nativeObject );
        }

        /** {@inheritDoc} */
        @Override
        public int hashCode()
        {
            return this.nativeObject.pointer().hashCode();
        }

        /** {@inheritDoc} */
        @Override
        public String toString()
        {
            return this.nativeObject.toString();
        }
    }

    private static final class PointerWrapper implements Freeable
    {
        private final long address;

        PointerWrapper( final Pointer pointer )
        {
            this.address = pointer.address();
        }

        @Override
        public void free()
        {
            DniInternal.ALLOCATOR.free( Pointer.fromAddress( this.address ) );
        }

        /** {@inheritDoc} */
        @Override
        public boolean equals( final Object obj )
        {
            if ( obj == null || obj.getClass() != getClass() )
            {
                return false;
            }
            if ( obj == this )
            {
                return true;
            }
            PointerWrapper other = (PointerWrapper) obj;
            return this.address == other.address;
        }

        /** {@inheritDoc} */
        @Override
        public int hashCode()
        {
            return (int) this.address ^ (int) ( this.address >>> 32 );
        }

        /** {@inheritDoc} */
        @Override
        public String toString()
        {
            return "0x" + Long.toHexString( this.address );
        }
    }

}
